﻿// MetroTile JavaScript library v1.0.0
// (c) Lance Wynn - http://metrotile.codeplex.com/
// License: MIT (http://www.opensource.org/licenses/mit-license.php)

//The tile can have 3 states displaying either the front, back, or peek (which shows half of each
var tileStates = {
    front: "front",
    back: "back",
    peek: "peek"
};

//The tile can display the front on top, bottom, left or right of the back
var tileDirections = {
    top: "top",
    bottom: "bottom",
    left: "left",
    right: "right"
};

//the tile can switch between front and back by sliding, or rotating.
var tileTransitions = {
    slide: "slide",
    rotate: "rotate"
};

////Got this script from https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
////so there is some back compatibility with older browsers.
if (!Function.prototype.bind) {
    Function.prototype.bind = function (oThis) {
        if (typeof this !== "function") {
            // closest thing possible to the ECMAScript 5 internal IsCallable function
            throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
        }

        var aArgs = Array.prototype.slice.call(arguments, 1),
            fToBind = this,
            fNOP = function () { },
            fBound = function () {
                return fToBind.apply(this instanceof fNOP && oThis
                                       ? this
                                       : oThis,
                                     aArgs.concat(Array.prototype.slice.call(arguments)));
            };

        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();

        return fBound;
    };
}

////////////////////////////////////////////////////////////////////////////////////
//Behavior that gets attached to the element that causes it to act like a live tile
////////////////////////////////////////////////////////////////////////////////////
var metroTileBehavior = function (element, options) {
    this.defaultOptions = {
        duration: 250,
        delay: 5000,
        slideDirection: tileDirections.top,
        showFrontOnMouseEnter: true,
        isTileActive: true,
        perspectiveDistance: 12,
        tileTransition: tileTransitions.rotate
    };

    $.extend(this.defaultOptions, options)

    this.tileElement = $(element);
    //Remove all no-tile classes from the element.
    this.tileElement.removeClass(function (index, css) {
        return (css.match(/\bno-tile+/g) || []).join(' ');
    });
    this.tileElement.addClass("metro-tile-container");
    this.tileFront = $(element).children().eq(0);
    this.tileFront.addClass("metro-tile");
    this.tileFront.width(this.tileElement.width());
    this.tileFront.height(this.tileElement.height());
    this.tileBack = $(element).children().eq(1);
    this.tileBack.addClass("metro-tile");

    this.tileBack.width(this.tileElement.width());
    this.tileBack.height(this.tileElement.height());
    this.state = tileStates.front;

    this.setProperty = function (propertyName, value) {
        this.defaultOptions[propertyName] = value;
    }

    this.setOptions = function (options) {
        $.extend(this.defaultOptions, options)
    }

    this.cancelCurrentTransition = function () {
        this.tileFront.stop();
        this.tileBack.stop();
    };

    this.showFront = function (duration) {
        if (!isElementInView(this.tileElement)) duration = 0; //only animate tile transitions if the element is in the visible space
        this.cancelCurrentTransition();
        if (!$.isNumeric(duration)) {
            duration = 0;
        }

        if (this.state == tileStates.back && this.defaultOptions.tileTransition == tileTransitions.rotate && duration > 0) {
            this._rotateToFront(duration);
        } else {
            this.tileFront.animate(this._getMargin('0'), duration);
            this.tileBack.animate(this._getMargin('100%'), duration);
        }

        this.state = tileStates.front;
    };

    this.showBack = function (duration) {
        if (!isElementInView(this.tileElement)) duration = 0; //only animate tile transitions if the element is in the visible space
        this.cancelCurrentTransition();
        if (!$.isNumeric(duration)) {
            duration = 0;
        }
        if (this.state == tileStates.front && this.defaultOptions.tileTransition == tileTransitions.rotate && duration > 0) {
            this._rotateToBack(duration);
        } else {
            this.tileFront.animate(this._getMargin('-100%'), duration);
            this.tileBack.animate(this._getMargin('0'), duration);
        }

        this.state = tileStates.back;
    };

    this._rotateToBack = function (duration) {
        var perspective = this.tileElement.height() * this.defaultOptions.perspectiveDistance;
        var frontElement = this.tileFront;
        var backElement = this.tileBack;
        var tile = this.tileElement;
        var rotateType = this.defaultOptions.slideDirection == tileDirections.top || this.defaultOptions.slideDirection == tileDirections.bottom ? "rotateX" : "rotateY";


        animateScalar(0, 90, duration / 2, null, function (step) {
            applyCSSToAllVendors(tile, 'transform', 'perspective(' + perspective + 'px) ' + rotateType + '(' + step + 'deg)');
        }.bind(this),
        function () {
            frontElement.animate(this._getMargin('-100%'), 0);
            backElement.animate(this._getMargin('0'), 0);

            animateScalar(-90, 0, duration / 2, null, function (step) {
                applyCSSToAllVendors(tile, 'transform', 'perspective(' + perspective + 'px) ' + rotateType + '(' + step + 'deg)');
            })
        }.bind(this))
    };

    this._rotateToFront = function (duration) {
        var perspective = this.tileElement.height() * this.defaultOptions.perspectiveDistance;
        var frontElement = this.tileFront;
        var backElement = this.tileBack;
        var tile = this.tileElement;
        var rotateType = this.defaultOptions.slideDirection == tileDirections.top || this.defaultOptions.slideDirection == tileDirections.bottom ? "rotateX" : "rotateY";

        animateScalar(0, -90, duration / 2, null, function (step) {
            applyCSSToAllVendors(tile, 'transform', 'perspective(' + perspective + 'px) ' + rotateType + '(' + step + 'deg)');
        }.bind(this),
        function () {
            frontElement.animate(this._getMargin('0'), 0);
            backElement.animate(this._getMargin('100%'), 0);

            animateScalar(90, 0, duration / 2, null, function (step) {
                applyCSSToAllVendors(tile, 'transform', 'perspective(' + perspective + 'px) ' + rotateType + '(' + step + 'deg)');
            })
        }.bind(this))
    }

    this.peek = function (duration) {
        if (!isElementInView(this.tileElement)) duration = 0; //only animate tile transitions if the element is in the visible space
        this.cancelCurrentTransition();
        if (!$.isNumeric(duration)) {
            duration = 0;
        }
        this.state = tileStates.peek;
        this.tileFront.animate(this._getMargin('-50%'), duration);
        this.tileBack.animate(this._getMargin('50%'), duration);
    };

    this._getMargin = function (pct) {
        var margin = {};
        margin[this.defaultOptions.slideDirection] = pct;
        var measure = (this.defaultOptions.slideDirection == tileDirections.left || this.defaultOptions.slideDirection == tileDirections.right)
            ? this.tileElement.width() : this.tileElement.height();
        switch (pct) {
            case '-100%':
                margin[this.defaultOptions.slideDirection] = '-' + measure + 'px';
                break;
            case '100%':
                margin[this.defaultOptions.slideDirection] = measure + 'px';
                break;
            case '-50%':
                margin[this.defaultOptions.slideDirection] = '-' + (measure / 2) + 'px';
                break;
            case '50%':
                margin[this.defaultOptions.slideDirection] = (measure / 2) + 'px';
                break;
            default:
                margin[this.defaultOptions.slideDirection] = '0px';
        }
        return margin;
    }

    this.toggleFrontBack = function (duration) {
        if (this.state == tileStates.front) {
            this.showBack(this.defaultDuration);
        } else {
            this.showFront(this.defaultDuration);
        }
    };

    this.setContent = function (tile, content, duration) {
        var content = $(tile.content);
        content.fadeOut(duration, function () {
            content.replaceWith(content);
            content.fadeIn(duration);
        });
    };

    this.setFrontContent = function (content, duration) {
        this.setContent(this.tileFront, content, duration);
    };

    this.setBackContent = function (content, duration) {
        this.setContent(this.tileBack, content, duration);
    };

    this._randomizeActiveState = function (duration) {
        if (this.isActive) {
            if (!$.isNumeric(duration)) {
                duration = this.defaultOptions.duration;
            }

            duration = Math.ceil(Math.random() * this.defaultOptions.duration) + this.defaultOptions.duration;

            var rnd = Math.ceil(Math.random() * 3)
            switch (rnd) {
                case 1:
                    if (this.state != tileStates.front) {
                        this.showFront(duration);
                    }
                    break;
                case 2:
                    if (this.state != tileStates.back) {
                        this.showBack(duration);
                    }
                    break;
                case 3:
                    if (this.state != tileStates.peek) {
                        this.peek(duration);
                    }
                    break;

            }
            this.activate(this.isActive);
        }
    }

    this.activate = function (boolVal) {
        this.isActive = boolVal;
        if (this.isActive) {
            delayTime = Math.ceil(Math.random() * this.defaultOptions.delay) + this.defaultOptions.delay;
            setTimeout(function () { this._randomizeActiveState(); }.bind(this), delayTime);
        }
    }

    //this.activate(this.defaultOptions.isTileActive);
    this.isActive = this.defaultOptions.isTileActive;

    if (this.defaultOptions.showFrontOnMouseEnter) {
        this.tileElement.mouseenter(function () {
            if (this.defaultOptions.showFrontOnMouseEnter) {
                this.showFront(100);
            }
        }.bind(this));
    }

    this.showFront(0);
    this._randomizeActiveState(this.defaultOptions.duration);
};
/////////////////////////////////////////////////////////////////////////////////////////////////////////
//End of metroTile behavior
/////////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////
//Behavior that gets attached to the element that causes it to act like a tile group
////////////////////////////////////////////////////////////////////////////////////
var metroTileGroupBehavior = function (element, options) {
    this.defaultOptions = {
        flyinDuration: 500,
        tileOptions: {
            duration: 250,
            delay: 5000,
            slideDirection: tileDirections.top,
            showFrontOnMouseEnter: true,
            isTileActive: false,
            perspectiveDistance: 12,
            tileTransition: tileTransitions.rotate
        }
    };

    $.extend(this.defaultOptions, options);
    this.groupElement = $(element);
    this.groupElement.addClass("metro-tile-group-container");
    this.groupElement.children().css("opacity", 0).metroTile(this.defaultOptions.tileOptions);
    var tileCount = this.groupElement.children().length;
    var tileAnimationDelay = this.defaultOptions.flyinDuration / tileCount;
    var tileRotationDuration = this.defaultOptions.flyinDuration / 2;
    $((this.groupElement.children()).get().reverse()).each(function (i) {
        var tile = $(this)
        
        tile.delay(i * tileAnimationDelay).animate({ opacity: 1 }, tileRotationDuration);

        setTimeout(function () {
            applyCSSToAllVendors(tile, 'transform-origin', '0% 50%');
            animateScalar(90, 0, tileRotationDuration, 'swing', function (step) {
                applyCSSToAllVendors(tile, 'transform', 'perspective(100px) rotateY(' + step + 'deg)');
            },
            function () {
                applyCSSToAllVendors(tile, 'transform-origin', '50% 50%');
                tile.metroTile(function (metroTile) {
                    metroTile.activate(true);
                });
            });

        }, i * tileAnimationDelay);
    });

    applyCSSToAllVendors($(element), 'transform-origin', '0% 50%');
    animateScalar(90, 0, this.defaultOptions.flyinDuration, 'swing', function (step) {
        applyCSSToAllVendors($(element), 'transform', 'perspective(200px) rotateY(' + step + 'deg)');
    });
};
/////////////////////////////////////////////////////////////////////////////////////////////////////////
//End of metroTileGroup behavior
/////////////////////////////////////////////////////////////////////////////////////////////////////////


//jQuery Extension for metroTile
(function ($) {
    $.fn.metroTile = function (options) {
        return this.each(function () {
            if ($(this).data('metroTile')) {
                ///If a function is passed to the metroTile extension, just invoke it with the 
                ///parameter being the metroTile behavior.
                if ($.isFunction(options)) {
                    options($(this).data('metroTile'));
                } else { ////Otherwise, update the options with the object passed in.
                    var tileBehavior = $(this).data('metroTile');
                    tileBehavior.setOptions(options);
                }
            } else {///If the tileBehavior is non-existant, then add it.
                var tileBehavior = new metroTileBehavior(this, options);
                $(this).data('metroTile', tileBehavior);
            }
        });
    };
})(jQuery);

//jQuery Extension for metroTileGroup 
(function ($) {
    $.fn.metroTileGroup = function (options) {
        return this.each(function () {
            if ($(this).data('metroTileGroup')) {
                ///If a function is passed to the metroTile extension, just invoke it with the 
                ///parameter being the metroTile behavior.
                if ($.isFunction(options)) {
                    options($(this).data('metroTileGroup'));
                } else { ////Otherwise, update the options with the object passed in.
                    var groupBehavior = $(this).data('metroTileGroup');
                    groupBehavior.setOptions(options);
                }
            } else {///If the tileBehavior is non-existant, then add it.
                var groupBehavior = new metroTileGroupBehavior(this, options);
                $(this).data('metroTileGroup', groupBehavior);
            }
        });
    };
})(jQuery);

///Utility functions, I should ultimately namespace these...
function animateScalar(from, to, duration, easing, stepCallback, completedCallback) {
    if (isNaN(from) || isNaN(to) || isNaN(duration) || !isFunction(stepCallback)) {
        console.log("Invalid animation parameter.");
    } else {
        var startTime = Date.now();
        var endTime = Date.now() + duration;
        var animationExtent = to - from;

        if (!easing) {
            easing = 'swing';
        }

        stepCallback(from);
        var workerFunc = function () {
            if (Date.now() > endTime) {
                stepCallback(to);
                if (isFunction(completedCallback)) {
                    completedCallback();
                }
            } else {
                var pctComplete = ((Date.now() - startTime) / duration);
                var msComplete = (Date.now() - startTime);
                
                var easingMethod
                if (easing) {
                    easingMethod = $.easing[easing];
                }

                var step;
                if (easingMethod) {
                    step = easingMethod(pctComplete, msComplete, from, animationExtent);
                }

                stepCallback(step);
                setTimeout(workerFunc, 10);
            }
        }
        setTimeout(workerFunc, 10);
    }
};

var vendorPrefixes = ['', '-ms-', '-webkit-'];

function applyCSSToAllVendors(element, name, value) {
    for (var i = 0; i < vendorPrefixes.length; i++) {
        element.css(vendorPrefixes[i] + name, value);
    }
};

function isElementInView(element) {
    var windowTop = $(window).scrollTop();
    var windowBottom = windowTop + $(window).height();

    var elementTop = element.offset().top;
    var elementBottom = elementTop + element.height();

    return elementBottom >= windowTop && elementTop <= windowBottom;
}

function isFunction(functionToCheck) {
    var getType = {};
    return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
}